﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Drawing;

namespace SCG.TurboSprite.TileBased
{
    public class MapSet
    {
        //Constructor
        public MapSet()
        {
        }
        public MapSet(string imagePath)
        {
            ImagePath = imagePath;
        }

        //Access the tiles
        public List<Tile> Tiles
        {
            get
            {
                return _tiles;
            }
            set
            {
                _tiles = value;
            }
        }

        //Access root path for images
        public string ImagePath { get; set; }

        //Map decorations
        public List<MapDecoration> Decorations
        {
            get
            {
                return _decorations;
            }
            set
            {
                _decorations = value;
            }
        }

        //Access shore segments
        [XmlIgnore]
        public List<List<Segment>> ShorePaths
        {
            get
            {
                return _shorePaths;
            }
        }

        //Access hard-coded adjacencies (Name,Name)
        public List<string> Adjacencies
        {
            get
            {
                return _adjacencies;
            }
            set
            {
                _adjacencies = value;
            }
        }

        //Dimensions of the map
        public int MapWidth
        {
            get
            {
                return _width;
            }
            set
            {
                _width = value;
            }
        }
        public int MapHeight
        {
            get
            {
                return _height;
            }
            set
            {
                _height = value;
            }
        }

        //Find tile
        public Tile FindTile(string name)
        {
            foreach (Tile t in Tiles)
                if (t.Name == name)
                    return t;
            return null;
        }
        public Tile FindTile(Point pt)
        {
            Region rgn = new Region();
            lock (_tiles)
            {
                foreach (Tile tile in _tiles)
                    if (PointInPolygon(pt, tile.Points))
                        return tile;
            }
            return null;
        }

        //Find a tile by segment and type
        public Tile FindTile(Segment s, TileType tt)
        {
            foreach(Tile tile in Tiles)
                if (tile.TileType == tt && tile.Segments.Contains(s))
                    return tile;
            return null;
        }

        //Offset all tiles by a specified amount
        public void OffsetAllTiles(int x, int y)
        {
            lock (_tiles)
            {
                foreach (Tile tile in _tiles)
                {
                    Point title = tile.TitleLocation;
                    title.X += x;
                    title.Y += y;
                    tile.TitleLocation = title;
                    for (int p = 0; p < tile.Points.Count; p++)
                    {
                        Point pt = tile.Points[p];
                        pt.X += x;
                        pt.Y += y;
                        tile.Points[p] = pt;
                    }
                    tile.Recalculate();               
                }
            }
            foreach (MapDecoration md in Decorations)
            {
                md.X += x;
                md.Y += y;
            }
        }

        //Populate adjacencies for all tiles
        public void PerformCalculations()
        {
            //Clear earlier data
            _shorePaths.Clear();
            foreach (Tile t in Tiles)
            {
                t.Adjacent.Clear();
                t.Segments.Clear();
            }

            //determine which zones are adjacent to each other
            foreach(Tile t1 in Tiles)
                foreach(Tile t2 in Tiles)
                    if (t1 != t2)
                        foreach(Segment s1 in t1.Segments)
                        {
                            bool adj = false;
                            foreach(Segment s2 in t2.Segments)
                                if (s1 == s2)
                                {
                                    adj = true;
                                    t1.Adjacent.Add(t2);
                                    break;
                                }
                            if (adj)
                                break;
                        }

            //Create a list of shore paths that represent all contiguous shores on the map
            //Step one - collect a list of unique shore segments
            List<Segment> shoreSegments = new List<Segment>();
            foreach(Tile seaZone in Tiles)
                if (seaZone.TileType == TileType.Sea)
                    foreach(Tile landZone in seaZone.Adjacent)
                        if (landZone.TileType == TileType.Land)
                        {
                            foreach (Segment seg in seaZone.Segments)
                                if (landZone.Segments.Contains(seg))
                                    if (!shoreSegments.Contains(seg))
                                        shoreSegments.Add(seg);
                        }

            //step two - isolate individual contiguous shore paths
            while (shoreSegments.Count > 0)
            {
                //Begin by starting at Point2 of first segment
                List<Segment> shorePath = new List<Segment>();
                Segment seg = shoreSegments[0];
                shoreSegments.RemoveAt(0);
                shorePath.Add(seg);

                //Look for consecutive path where seg.Point2 connects -> other
                bool foundConnector = true;
                while (foundConnector)
                {
                    foundConnector = false;
                    foreach (Segment other in shoreSegments)
                    {
                        if (other.Point1 == seg.Point2)
                        {
                            foundConnector = true;
                            shoreSegments.Remove(other);
                            shorePath.Add(other);
                            seg = other;
                            break;
                        }
                        else if (other.Point2 == seg.Point2)
                        {
                            foundConnector = true;
                            shoreSegments.Remove(other);
                            Segment rev = other.Reversed;
                            shorePath.Add(rev);
                            seg = rev;
                            break;
                        }
                    }
                }

                //now connect the other way, seg.point1 -> other
                seg = shorePath[0];
                foundConnector = true;
                while (foundConnector)
                {
                    foundConnector = false;
                    foreach (Segment other in shoreSegments)
                    {
                        if (other.Point2 == seg.Point1)
                        {
                            foundConnector = true;
                            shoreSegments.Remove(other);
                            shorePath.Insert(0, other);
                            seg = other;
                            break;
                        }
                        else if (other.Point1 == seg.Point1)
                        {
                            foundConnector = true;
                            shoreSegments.Remove(other);
                            Segment rev = other.Reversed;
                            shorePath.Insert(0, rev);
                            seg = rev;
                            break;
                        }
                    }
                }

                //save the raw path
                _shorePaths.Add(shorePath);
            }

            //Add hard-coded adjacencies
            foreach (string adj in Adjacencies)
            {
                string[] tileNames = adj.Split(',');
                if (tileNames.Length == 2)
                {
                    Tile t1 = FindTile(tileNames[0]);
                    Tile t2 = FindTile(tileNames[1]);
                    if (t1 != null && t2 != null)
                    {
                        t1.Adjacent.Add(t2);
                        t2.Adjacent.Add(t1);
                    }
                }
            }
        }

        //Save the tiles to a file
        public void SaveToFile(string fileName)
        {
            XmlSerializer xmlSer = new XmlSerializer(typeof(MapSet));
            TextWriter writer = new StreamWriter(fileName);
            try
            {
                xmlSer.Serialize(writer, this);
            }
            finally
            {
                writer.Close();
            }
        }

        //Load tiles from a file
        public static MapSet LoadFromFile(string fileName, TileBasedSpriteSurface surface, string imagePath)
        {
            MapSet ms = null;          
            XmlSerializer xmlSer = new XmlSerializer(typeof(MapSet));
            TextReader reader = new StreamReader(fileName);
            try
            {
                ms = (MapSet)xmlSer.Deserialize(reader);
                ms.ImagePath = imagePath;
            }
            finally
            {
                reader.Close();
            }
            int idx = 0;
            foreach (Tile tile in ms.Tiles)
            {
                tile._index = idx++;
                tile.Surface = surface;
            }
            foreach (MapDecoration decor in ms.Decorations)
                decor.MapSet = ms;
            ms.PerformCalculations();
            return ms;
        }

        //Determine if a point is in a polygon
        public static bool PointInPolygon(Point p, IList<Point> poly)
        {
            Point p1, p2;
            bool inside = false;
            if (poly.Count < 3)
            {
                return inside;
            }
            Point oldPoint = new Point(
            poly[poly.Count - 1].X, poly[poly.Count - 1].Y);
            for (int i = 0; i < poly.Count; i++)
            {
                Point newPoint = new Point(poly[i].X, poly[i].Y);
                if (newPoint.X > oldPoint.X)
                {
                    p1 = oldPoint;
                    p2 = newPoint;
                }
                else
                {
                    p1 = newPoint;
                    p2 = oldPoint;
                }
                if ((newPoint.X < p.X) == (p.X <= oldPoint.X)
                && ((long)p.Y - (long)p1.Y) * (long)(p2.X - p1.X)
                 < ((long)p2.Y - (long)p1.Y) * (long)(p.X - p1.X))
                {
                    inside = !inside;
                }
                oldPoint = newPoint;
            }
            return inside;
        }

        //and if a rectangle is contained in a polygon
        public static bool RectInPolygon(Rectangle rect, IList<Point> poly)
        {
            if (!PointInPolygon(new Point(rect.Left, rect.Top), poly))
                return false;
            if (!PointInPolygon(new Point(rect.Right, rect.Top), poly))
                return false;
            if (!PointInPolygon(new Point(rect.Left, rect.Bottom), poly))
                return false;
            if (!PointInPolygon(new Point(rect.Right, rect.Bottom), poly))
                return false;
            return true;
        }

        //Populate a path from start zone to end zone, and return the cost to traverse
        public Path FindPath(Tile startTile, Tile endTile, double maxCost)
        {
            if (startTile == null || endTile == null)
                return null;
            Path path = new Path(startTile, endTile);            
            path.Tiles.Add(startTile);

            //if start equals end, return end
            if (startTile == endTile)            
                return path;            

            //Short circuit if its adjacent
            if (startTile.IsAdjacent(endTile))
            {              
                path.Cost = startTile.CostToTraverse(endTile);
                path.Tiles.Add(endTile);
                return path;
            }

            //Maintain a list of all tiles we considered already and ones we are currently testing
            List<Tile> considered = new List<Tile>();
            List<Tile> considering = new List<Tile>();
            List<Tile> newConsider = new List<Tile>();
            Path bestPath = null;
            double bestCost = Double.MaxValue;
            
            //Begin with the first tile
            startTile.PathExplored = path;
            considering.Add(startTile);

            //Flag that detects when no better paths were found
            bool betterPathFound = true;
            while (betterPathFound)
            {
                betterPathFound = false;

                //move the tiles being considered into the "considered" list
                newConsider.Clear();
                foreach (Tile c in considering)
                    considered.Add(c);

                //test the tiles being considered
                foreach (Tile c in considering)
                {
                    //consider adjacent tiles that have not already been considered
                    foreach(Tile adj in c.Adjacent)
                        if (!considered.Contains(adj))
                        {
                            //clone the path and add this tile to the end
                            Path p = new Path(c.PathExplored);
                            p.Tiles.Add(adj);
                            adj.PathExplored = p;
                            int tc = p.Tiles.Count;
                            p.Cost += p.Tiles[tc - 2].CostToTraverse(p.Tiles[tc - 1]);

                            //is the path still viable?
                            if (p.Cost < bestCost && p.Cost < Double.MaxValue)
                            {
                                //yes, consider this tile next                           
                                betterPathFound = true;

                                //is the path completed?
                                if (p.Completed)
                                {
                                    //best so far?
                                    if (p.Cost < bestCost)
                                    {
                                        bestPath = p;
                                        bestCost = p.Cost;
                                    }
                                }
                                //no, but consider it for next round
                                else
                                    newConsider.Add(adj);
                            }                            
                        }
                }

                //update the list of tiles being considered
                considering.Clear();
                foreach (Tile n in newConsider)
                    considering.Add(n);
            }       

            //Return the best one found
            return bestPath;
        }
        
        //private members
        private List<Tile> _tiles = new List<Tile>();
        private int _width;
        private int _height;
        private List<List<Segment>> _shorePaths = new List<List<Segment>>();
        private List<MapDecoration> _decorations = new List<MapDecoration>();
        private List<string> _adjacencies = new List<string>();  
    }
}
